Reactive forms的驗證大多是直接寫在controller裡的,會是一個明確的、非UI的data flowing。
Reactive forms的reactive patterns可以讓測試與驗證更加簡單。
使用Reactive forms可以用一個樹狀的控制物件來binding到表單template的元件上,這讓所有驗證的程式碼都集中在一起,方便維護與管理,在撰寫單元測試時也會較為容易。
使用Model-Driven Forms也較符合reactive programming的概念(延伸閱讀:Functional Reactive Programming 的入門心得)
Template-driven forms是將組件驗證控制的功能寫在像是<input>
或<select>
的標籤內,並利用ngModel來確認是否輸入了合法的內容。
使用表單驅動驗證不需要自己創建control objects,因為angular已經為我們建好了。
ngModel會處理使用者改變與輸入表單的事件,並更新ngModel裡面的可變數據,讓我們可以去處理後續的事。
也因此ngModel並不是ReactiveFormsModule
的一部份。
這代表著使用表單驅動驗證,我們需要撰寫的程式碼更少。
但是如果我們的表單需要很複雜的驗證步驟並且要顯示很多不同的錯誤訊息時,使用表單驅動驗證會使事情變得更複雜並難以維護。
Reactive forms是同步的而Template-driven forms為非同步處理,是這兩者間最大的差異。
對Reactive forms來說,所有表單的資料是在code裡以tree的方式來呈現,所以在任一個節點可以取得其他表單的資料,並且這些資料是即時同步被更新的。我們也可以在使用者修改了某個input的值時,去為使用者自動update另一個input內的預設值,這是因為所有資料都是隨時可取得的。
Template-driven forms在每一個表單元件各自透過directive委派檢查的功能,為了避免檢查後修改而造成檢查失效的問題,directive會在更多的時後去檢查輸入的值的正確性,因此並沒有辦法立即的得到回應,而需要一小段的時間才有辦法得到使用者輸入的值是否合法的回應。這會讓我們在撰寫單元測試時更加複雜,我們會需要利用setTimeout
去讓取得的檢查結果是正確的
Reactive Forms的功能封裝在ReactiveFormsModule中,和FormsModule同樣在@angular/forms
之下。
如果要使用Reactive Forms需要使用下面的程式碼
import { ReactiveFormsModule } from '@angular/forms';
首先要新增FormGroup所需使用的類別
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
然後創建這個Group,並定義裡面的驗證元素
export class HeroDetailComponent2 {
heroForm = new FormGroup ({
name: new FormControl()
});
}
接著,在template裡面的form裡指定這個form要使用heroForm來做表單驗證,並且在input裡面指定他的formControlName
<h2>Hero Detail</h2>
<h3><i>FormControl in a FormGroup</i></h3>
<form [formGroup]="heroForm" novalidate>
<div class="form-group">
<label class="center-block">Name:
<input class="form-control" formControlName="name">
</label>
</div>
</form>
form標籤下的novalidate
屬性,是為了要防止瀏覽器自己執行native的驗證。
<form [formGroup]="heroForm" novalidate>
而[formGroup]="heroForm"
則是將template內的form元件與controller裡所創的formGroup做關連
<input class="form-control" formControlName="name">
這個則是將input
與formGroup
下名為name
的formControl
做關連。
註:bootstrap的form-group以及form-control與angular完全無關。下面是bootstrap為我們設計的form表單樣式範例,但是這只是css,沒辦法讓組件與控制器結合。
<form>
<div class="form-group">
<label for="formGroupExampleInput">Example label</label>
<input type="text" class="form-control" id="formGroupExampleInput" placeholder="Example input">
</div>
<div class="form-group">
<label for="formGroupExampleInput2">Another label</label>
<input type="text" class="form-control" id="formGroupExampleInput2" placeholder="Another input">
</div>
</form>
FormBuilder可以減少我們在創建formGroup時有太多重覆的定義,要使用要先import必要的檔案
import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
使用formBuild大致要做的事如下:
this.fb.group
來宣告這個formGroup裡所有的formControl。export class HeroDetailComponent3 {
heroForm: FormGroup; // <--- 宣告heroForm為FormGroup
constructor(private fb: FormBuilder) { // <--- 注入FormBuilder
this.createForm();
}
createForm() {
this.heroForm = this.fb.group({
name: '', // <--- 建立一個名為name,預設值為''的formControl
});
}
}
formBuild的宣告方式如上,name控件由其初始數據值(一個空字符串)定義。
首先要先import該驗證器
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
然後在建立formControl時指定使用該驗證器
this.heroForm = this.fb.group({
name: ['', Validators.required ],
});
有時我們在做地址輸入框時,會有如國家、區、鄉、市、街、郵遞區號等不同的輸入欄位,但他們應該是一個group,這時候就可以用nested formGroup。透過這樣的結構層次,可以讓我們在追蹤表格狀態更為容易清楚。
export class HeroDetailComponent5 {
heroForm: FormGroup;
states = states;
constructor(private fb: FormBuilder) {
this.createForm();
}
createForm() {
this.heroForm = this.fb.group({ // <-- the parent FormGroup
name: ['', Validators.required ],
address: this.fb.group({ // <-- the child FormGroup
street: '',
city: '',
state: '',
zip: ''
}),
power: '',
sidekick: ''
});
}
}
我們用一個div將整個地址的區塊包起來,並用formGroupName="address"
來與heroForm裡的address做連結
<div formGroupName="address" class="well well-lg">
<h4>Secret Lair</h4>
<div class="form-group">
<label class="center-block">Street:
<input class="form-control" formControlName="street">
</label>
</div>
<div class="form-group">
<label class="center-block">City:
<input class="form-control" formControlName="city">
</label>
</div>
<div class="form-group">
<label class="center-block">State:
<select class="form-control" formControlName="state">
<option *ngFor="let state of states" [value]="state">{{state}}</option>
</select>
</label>
</div>
<div class="form-group">
<label class="center-block">Zip Code:
<input class="form-control" formControlName="zip">
</label>
</div>
</div>
我們可以用下面的方式來將formControl裡可使用的屬性都印出來
<p>Form value: {{ heroForm.value | json }}</p>
基本上會有下面四個屬性可以讓我們使用
屬性 | 描述 |
---|---|
myControl.value | FormControl使用者輸入的值 |
myControl.status | 這個FormControl的驗證結果. 可能的值有: VALID, INVALID, PENDING, or DISABLED. |
myControl.pristine | 使用者是否有在UI上更動過這個元素,假如沒有的話會是true。相反的屬性為myControl.dirty. |
myControl.untouched | 使用者尚未輸入並且從未觸發過blur event時為true。相反的屬性為 myControl.touched. |
以往我們在創建資料類型時是像這樣子的
export class Hero {
id = 0;
name = '';
addresses: Address[];
}
export class Address {
street = '';
city = '';
state = '';
zip = '';
}
但是我們在創建formGroup是這樣子的
this.heroForm = this.fb.group({
name: ['', Validators.required ],
address: this.fb.group({
street: '',
city: '',
state: '',
zip: ''
}),
power: '',
sidekick: ''
});
我們可以直接利用class來創建formControl
this.heroForm = this.fb.group({
name: ['', Validators.required ],
address: this.fb.group(new Address()), // <-- a FormGroup with a new address
power: '',
sidekick: ''
});
在上面data model和form model的介紹範例中,可以看到Hero與formGroup建立heroForm
模型有兩個顯著的區別:
但是我們可以利用setValue來更簡單的將一個class的資料填進表單中。
this.heroForm.setValue({
name: this.hero.name,
address: this.hero.addresses[0] || new Address()//這是因為form元件只能顯示一個地址,如果class內容沒有值時,要預設新建立一個Address物件
});
也可以使用patchValue來將單一的值填入表單裡
this.heroForm.patchValue({
name: this.hero.name
});
如果我們要做一個修改hero資料的列表,當點下某個hero時就可以修改該hero的資料
<nav>
<a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a>
</nav>
<div *ngIf="selectedHero">
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
</div>
然後在controller裡面去監聽ngOnChange事件並且用setValue來設定要修改的值
ngOnChanges() {
this.heroForm.reset({
name: this.hero.name,
address: this.hero.addresses[0] || new Address()
});
}
會需要使用reset是為了要清除前一個hero的資料
如果一個hero可能需要有多組的地址時,就會需要使用formArray。 原本我們是這樣定義Address的
this.heroForm = this.fb.group({
name: ['', Validators.required ],
address: this.fb.group(new Address()), // <-- a FormGroup with a new address
power: '',
sidekick: ''
});
使用formArray則變成這樣
this.heroForm = this.fb.group({
name: ['', Validators.required ],
secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray
power: '',
sidekick: ''
});
可以用下面的function將很多組的address設定進去formArray成為預設值
setAddresses(addresses: Address[]) {
const addressFGs = addresses.map(address => this.fb.group(address));
const addressFormArray = this.fb.array(addressFGs);
this.heroForm.setControl('secretLairs', addressFormArray);
}
要取得formArray可以撰寫下面的方法
get secretLairs(): FormArray {
return this.heroForm.get('secretLairs') as FormArray;
};
而顯示方式如下:
<div formArrayName="secretLairs" class="well well-lg">
<div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
<!-- The repeated address template -->
</div>
</div>
完整內容如下:
<div formArrayName="secretLairs" class="well well-lg">
<div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
<!-- The repeated address template -->
<h4>Address #{{i + 1}}</h4>
<div style="margin-left: 1em;">
<div class="form-group">
<label class="center-block">Street:
<input class="form-control" formControlName="street">
</label>
</div>
<div class="form-group">
<label class="center-block">City:
<input class="form-control" formControlName="city">
</label>
</div>
<div class="form-group">
<label class="center-block">State:
<select class="form-control" formControlName="state">
<option *ngFor="let state of states" [value]="state">{{state}}</option>
</select>
</label>
</div>
<div class="form-group">
<label class="center-block">Zip Code:
<input class="form-control" formControlName="zip">
</label>
</div>
</div>
<br>
<!-- End of the repeated address template -->
</div>
</div>
要為這個hero新增一個地址可以用下面這個方法
addLair() {
this.secretLairs.push(this.fb.group(new Address()));
}
按下增加地址按鈕時呼叫這個方法
<button (click)="addLair()" type="button">Add a Secret Lair</button>
將formControl的資料用深層複製存回class裡的方法
prepareSaveHero(): Hero {
const formModel = this.heroForm.value;
// deep copy of form model lairs
const secretLairsDeepCopy: Address[] = formModel.secretLairs.map(
(address: Address) => Object.assign({}, address)
);
// return new `Hero` object containing a combination of original hero value(s)
// and deep copies of changed form model values
const saveHero: Hero = {
id: this.hero.id,
name: formModel.name as string,
// addresses: formModel.secretLairs // <-- bad!
addresses: secretLairsDeepCopy
};
return saveHero;
}